package bsh.utils.reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.TreeMap;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

/**
 * Classe auxiliar para facilitar boxing / unboxing de primitivas
 * 
 * @author takeshi
 */
public final class BytecodeHelper implements Opcodes {

    /**
     * C'tor privado
     */
    private BytecodeHelper() {
    }

    private static final int MAX_INT_CONST = 5;

    private static final String JAVA_LANG_DOUBLE = "java/lang/Double";

    private static final String JAVA_LANG_FLOAT = "java/lang/Float";

    private static final String JAVA_LANG_LONG = "java/lang/Long";

    private static final String JAVA_LANG_BOOLEAN = "java/lang/Boolean";

    private static final String JAVA_LANG_INTEGER = "java/lang/Integer";

    private static final String JAVA_LANG_BYTE = "java/lang/Byte";

    private static final String JAVA_LANG_SHORT = "java/lang/Short";

    private static final String JAVA_LANG_CHAR = "java/lang/Character";

    private static final String JAVA_LANG_NUMBER = "java/lang/Number";

    /**
     * @param visitor {@link MethodVisitor} host
     * @param sourceType o tipo primitivo
     */
    public static void autoBox(MethodVisitor visitor, Class<?> sourceType) {
        if (sourceType == byte.class) {
            boxByte(visitor);
        } else if (sourceType == short.class) {
            boxShort(visitor);
        } else if (sourceType == char.class) {
            boxChar(visitor);
        } else if (sourceType == float.class) {
            boxFloat(visitor);
        } else if (sourceType == double.class) {
            boxDouble(visitor);
        } else if (sourceType == long.class) {
            boxLong(visitor);
        } else if (sourceType == int.class) {
            boxInt(visitor);
        } else if (sourceType == boolean.class) {
            boxBoolean(visitor);
        } else {
            throw new IllegalStateException("type not treated: " + sourceType);
        }
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void boxLong(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_LONG, "valueOf", "(J)Ljava/lang/Long;");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void boxDouble(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_DOUBLE, "valueOf", "(D)Ljava/lang/Double;");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void boxFloat(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_FLOAT, "valueOf", "(F)Ljava/lang/Float;");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void boxChar(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_CHAR, "valueOf", "(C)Ljava/lang/Character;");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void boxShort(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_SHORT, "valueOf", "(S)Ljava/lang/Short;");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void boxByte(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_BYTE, "valueOf", "(B)Ljava/lang/Byte;");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void boxBoolean(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_BOOLEAN, "valueOf", "(Z)Ljava/lang/Boolean;");
    }

    /**
     * Gera o codigo de unboxing
     * 
     * @param visitor {@link MethodVisitor}
     * @param targetType o tipo primitivo
     */
    public static void autoUnbox(MethodVisitor visitor, Class<?> targetType) {
        if (targetType == byte.class) {
            unboxByte(visitor);
        } else if (targetType == short.class) {
            unboxShort(visitor);
        } else if (targetType == char.class) {
            unboxChar(visitor);
        } else if (targetType == float.class) {
            unboxFloat(visitor);
        } else if (targetType == double.class) {
            unboxDouble(visitor);
        } else if (targetType == int.class) {
            unboxInteger(visitor);
        } else if (targetType == long.class) {
            unboxLong(visitor);
        } else if (targetType == boolean.class) {
            unboxBoolean(visitor);
        } else {
            throw new IllegalStateException("type not treated: " + targetType);
        }
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void unboxDouble(MethodVisitor visitor) {
        visitor.visitTypeInsn(CHECKCAST, JAVA_LANG_NUMBER);
        visitor.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_NUMBER, "doubleValue", "()D");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void unboxFloat(MethodVisitor visitor) {
        visitor.visitTypeInsn(CHECKCAST, JAVA_LANG_NUMBER);
        visitor.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_NUMBER, "floatValue", "()F");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void unboxChar(MethodVisitor visitor) {
        visitor.visitTypeInsn(CHECKCAST, JAVA_LANG_CHAR);
        visitor.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_CHAR, "charValue", "()C");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void unboxShort(MethodVisitor visitor) {
        visitor.visitTypeInsn(CHECKCAST, JAVA_LANG_NUMBER);
        visitor.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_NUMBER, "shortValue", "()S");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void unboxByte(MethodVisitor visitor) {
        visitor.visitTypeInsn(CHECKCAST, JAVA_LANG_NUMBER);
        visitor.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_NUMBER, "byteValue", "()B");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void unboxBoolean(MethodVisitor visitor) {
        final String owner = JAVA_LANG_BOOLEAN;
        visitor.visitTypeInsn(CHECKCAST, owner);
        visitor.visitMethodInsn(INVOKEVIRTUAL, owner, "booleanValue", "()Z");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    public static void boxInt(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_INTEGER, "valueOf", "(I)Ljava/lang/Integer;");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    public static void unboxInteger(MethodVisitor visitor) {
        // first element on the operand stack must be an Integer
        final String owner = JAVA_LANG_NUMBER;

        visitor.visitTypeInsn(CHECKCAST, owner);
        visitor.visitMethodInsn(INVOKEVIRTUAL, owner, "intValue", "()I");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    public static void unboxLong(MethodVisitor visitor) {
        final String internalName = JAVA_LANG_NUMBER;
        visitor.visitTypeInsn(CHECKCAST, internalName);
        visitor.visitMethodInsn(INVOKEVIRTUAL, internalName, "longValue", "()J");
    }

    /**
     * Cria um construtor vazio que invoca um construtor vazio da superclasse
     * 
     * @param cw {@link ClassWriter}
     * @param superType {@link Class}
     */
    public static void createEmptyConstructor(ClassWriter cw, Class<?> superType) {
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(superType), "<init>", "()V");
        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    /**
     * Sobrescreve o metodo passado
     * 
     * @param writer {@link ClassWriter}
     * @param targetMethod {@link Method}
     * @return {@link MethodVisitor}
     */
    public static MethodVisitor overwritePublicMethod(ClassWriter writer, Method targetMethod) {
        Class<?>[] types = targetMethod.getExceptionTypes();
        String[] exc = new String[types.length];
        for (int i = 0; i < types.length; i++) {
            Class<?> class1 = types[i];
            exc[i] = Type.getInternalName(class1);
        }
        return writer.visitMethod(ACC_PUBLIC, targetMethod.getName(), Type.getMethodDescriptor(targetMethod), null, exc);
    }

    /**
     * @param method the method
     * @return boolean
     */
    public static boolean isStatic(Method method) {
        return Modifier.isStatic(method.getModifiers());
    }

    /**
     * 'arruma' o topo da pilha. Empilha 'null' caso o metodo passado devolva void ou faz o boxing de primitivas
     * 
     * @param mv o {@link MethodVisitor}
     * @param method o {@link Method}
     */
    public static void fixReturnType(MethodVisitor mv, Method method) {
        if (method.getReturnType() == void.class || method.getReturnType() == Void.class) {
            // push null to fill the stack
            mv.visitInsn(ACONST_NULL);
            // or the method could return a primitive
        } else if (method.getReturnType().isPrimitive()) {
            // box the primitive
            autoBox(mv, method.getReturnType());
        }
    }

    /**
     * @param all all methods
     * @param arity the desired arity
     * @return {@link Map}
     */
    public static Map<Integer, Method> collectMethodsWithArity(Method[] all, int arity) {
        Map<Integer, Method> map = new TreeMap<Integer, Method>();
        for (int i = 0; i < all.length; i++) {
            Method method = all[i];
            if (method.getParameterTypes().length == arity) {
                map.put(i, method);
            }
        }
        return map;
    }

    /**
     * faz o 'push' de um inteiro.
     * 
     * @param constant o valor
     * @param mv o {@link MethodVisitor}
     */
    public static void pushIntConst(int constant, MethodVisitor mv) {
        if (constant >= -1 && constant <= MAX_INT_CONST) {
            mv.visitInsn(ICONST_M1 + constant + 1);
        } else if (constant >= Byte.MIN_VALUE && constant <= Byte.MAX_VALUE) {
            mv.visitIntInsn(BIPUSH, constant);
        } else if (constant >= Short.MIN_VALUE && constant <= Short.MAX_VALUE) {
            mv.visitIntInsn(SIPUSH, constant);
        } else {
            mv.visitLdcInsn(Integer.valueOf(constant));
        }
    }

    /**
     * Empurra uma constante no stack
     * 
     * @param val valor
     * @param mv mv
     */
    public static void pushDoubleConst(double val, MethodVisitor mv) {
        if (val == 0.0) {
            mv.visitInsn(DCONST_0);
        } else if (val == 1.0) {
            mv.visitInsn(DCONST_1);
        } else {
            mv.visitLdcInsn(val);
        }
    }

    /**
     * Empurra uma constante no stack
     * 
     * @param val valor
     * @param mv mv
     */
    public static void pushFloatConst(float val, MethodVisitor mv) {
        if (val == 0.0f) {
            mv.visitInsn(FCONST_0);
        } else if (val == 1.0f) {
            mv.visitInsn(FCONST_1);
        } else if (val == 2.0f) {
            mv.visitInsn(FCONST_2);
        } else {
            mv.visitLdcInsn(val);
        }
    }

    /**
     * Empurra uma constante no stack
     * 
     * @param val valor
     * @param mv mv
     */
    public static void pushLongConst(long val, MethodVisitor mv) {
        if (val == 0L) {
            mv.visitInsn(LCONST_0);
        } else if (val == 1L) {
            mv.visitInsn(LCONST_1);
        } else {
            mv.visitLdcInsn(val);
        }
    }

    /**
     * Faz o 'push' de um valor booleano
     * 
     * @param value boolean
     * @param mv {@link MethodVisitor}
     */
    public static void pushBooleanValue(boolean value, MethodVisitor mv) {
        pushIntConst(value ? 1 : 0, mv);
    }

    /**
     * Gera uma classe com um construtor vazio
     * 
     * @param clName o nome da classe
     * @param ctor Construtor da superclasse
     * @param ifs interfaces
     * @return {@link ClassWriter}
     */
    public static ClassWriter createClassWithEmptyConstructor(String clName, final Constructor<?> ctor, Class<?>... ifs) {
        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        String[] ifNames = new String[ifs.length];
        for (int i = 0; i < ifNames.length; i++) {
            ifNames[i] = Type.getInternalName(ifs[i]);
        }
        writer.visit(V1_5, ACC_PUBLIC + ACC_SUPER, clName.replaceAll("[.]", "/"), null, Type.getInternalName(ctor.getDeclaringClass()),
                ifNames);
        createEmptyConstructor(writer, ctor.getDeclaringClass());
        return writer;
    }


}